package net.nohaven.proj.javeau.fs.crypt;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import net.nohaven.proj.javeau.crypt.PwdUtils;
import net.nohaven.proj.javeau.crypt.engine.Phelix;
import net.nohaven.proj.javeau.crypt.prng.PRNG;
import net.nohaven.proj.javeau.crypt.streams.PhelixInputStream;
import net.nohaven.proj.javeau.crypt.streams.PhelixOutputStream;
import net.nohaven.proj.javeau.fs.FSUtils;
import net.nohaven.proj.javeau.fs.exceptions.ConditionsNotMetException;
import net.nohaven.proj.javeau.fs.exceptions.EntityAlreadyPresentException;
import net.nohaven.proj.javeau.fs.exceptions.EntityNotFoundException;
import net.nohaven.proj.javeau.fs.exceptions.GenericIOException;
import net.nohaven.proj.javeau.fs.exceptions.WrongPassException;
import net.nohaven.proj.javeau.fs.model.FSDirectory;
import net.nohaven.proj.javeau.fs.model.FSFileWriter;
import net.nohaven.proj.javeau.fs.model.FSItem;
import net.nohaven.proj.javeau.fs.model.Filesystem;
import net.nohaven.proj.javeau.utils.BitArrayInputStream;
import net.nohaven.proj.javeau.utils.BitArrayOutputStream;
import net.nohaven.proj.javeau.utils.Utils;

public class CFSFilesystem implements Filesystem {
	private File baseDir;
	private CFSDirectory root;
	private CFSDirectory pwd;
	private byte[] key;
	private byte[] password;

	protected static final byte[] MAGIC = new byte[] { 0x03, 0x45, 0x6C, 0x79 };

	protected static final String TOC_FILE = "index";
	private static final String BAK_FILE = TOC_FILE + ".bak";
	private static final String TMP_FILE = TOC_FILE + ".tmp";
	private static final String LOCK_FILE = "lock";

	public CFSFilesystem(File baseDir, char[] password, boolean useLock)
			throws ConditionsNotMetException, GenericIOException {
		this.baseDir = baseDir;
		this.root = new CFSDirectory(null, null);
		this.pwd = root;

		this.password = PwdUtils.strengthenPassword(new String(password)
				.getBytes(), 32);

		//ensures that the structures are there

		if (useLock) {
			File lock = new File(baseDir, LOCK_FILE);
			if (lock.exists())
				throw new ConditionsNotMetException(
						"Secure folder already in use.");

			//writes lock
			try {
				lock.createNewFile();
				lock.deleteOnExit();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		new File(baseDir, TMP_FILE).delete();

		if (CFSUtils.isSecretDirectory(baseDir)) {
			load();
			return;
		}

		//create directories

		CFSUtils.createDirs(baseDir);
		save();

	}

	private byte[] getKey() {
		if (key == null) {
			key = new byte[32];
			PRNG.getInstance().getBytes(key);
		}
		return key;
	}

	public void cd(String path) throws EntityNotFoundException {
		if ("..".equals(path)) {
			cdUp();
			return;
		}

		FSItem newPwd = pwd.get(path);
		if (newPwd == null || !FSUtils.isDir(newPwd))
			throw new EntityNotFoundException(
					"Path doesn't exist or isn't a directory");

		pwd = (CFSDirectory) newPwd;
	}

	public void cdUp() {
		if (pwd.getParent() != null)
			pwd = (CFSDirectory) pwd.getParent();
	}

	public void close() {
		save();

		new File(baseDir, LOCK_FILE).delete();
	}

	private final Runnable saveOnCommit = new Runnable() {
		public void run() {
			save();
		}
	};

	public FSFileWriter createFile(String name)
			throws EntityAlreadyPresentException, GenericIOException {
		if (pwd.contains(name))
			throw new EntityAlreadyPresentException("Entity already present");

		return new CFSFileWriter(getKey(), name, pwd, baseDir, saveOnCommit);
	}

	//imports an existing file in the filesystem
	private void anchorFile(String name, byte[] iv, byte[] mac) {

		File realFile = new File(baseDir, CFSUtils.iv2path(iv));

		CFSFile file = new CFSFile(realFile, name, key, iv, mac);

		pwd.add(file);
	}

	public void delete(String name) throws ConditionsNotMetException {
		FSItem fsi = pwd.get(name);
		if (fsi == null)
			return;

		if (FSUtils.isDir(fsi)) {
			if (!((CFSDirectory) fsi).isEmpty())
				throw new ConditionsNotMetException(
						"Directory deletion failed; it's not empty.");
		} else
			((CFSFile) fsi).delRealFile();

		pwd.remove(name);

		save();
	}

	public FSItem get(String name) {
		return pwd.get(name);
	}

	public FSDirectory getPwd() {
		return pwd;
	}

	public void goToRoot() {
		pwd = root;
	}

	public FSItem[] ls() {
		return pwd.getContents();
	}

	public void mkDir(String name) throws EntityAlreadyPresentException {
		if (pwd.contains(name))
			throw new EntityAlreadyPresentException("Entity already present");
		CFSDirectory dir = new CFSDirectory(pwd, name);
		pwd.add(dir);

		save();
	}

	public void rename(String name, String newName)
			throws EntityNotFoundException, EntityAlreadyPresentException {
		if (pwd.contains(newName))
			throw new EntityAlreadyPresentException("Entity already present");

		FSItem toRename = pwd.remove(name);
		if (toRename == null)
			throw new EntityNotFoundException("Entity not found");

		if (toRename instanceof CFSFile)
			((CFSFile) toRename).setName(newName);

		if (toRename instanceof CFSDirectory)
			((CFSDirectory) toRename).setName(newName);

		pwd.add(toRename);

		save();
	}

	//Input-output

	/* A save file is like this: (B = Byte, b = bit)
	 * <plaintext>
	 *  4B Magic number ( 0x03, 0x45, 0x6C, 0x79 )
	 * 16B IV for this file
	 * 
	 * <ciphertext>
	 *  4B Magic number ( For password verification )
	 * 16B random seed for the PRNG
	 * 32B encryption key for the files
	 * VAR filesystem structure (**)
	 *
	 * <MAC>
	 * 16B the MAC of this file
	 * 
	 * (**) Filesystem structure
	 * 
	 * Depth-first walk in the directory/file tree, omitting the root.
	 * 
	 * Chunk 1
	 * 4B length of chunk 1, in bytes, little endian
	 * 
	 * - if a file is encountered, output a '1' bit
	 * - if a directory, output a '00' (2b) and go down
	 * - if the current dir listing is finished, output a '01' and go up
	 * - pad with a single 1 and multiple 0's until 8 bit boundary
	 * 
	 * Chunk 2
	 * 4B length of chunk 2, in bytes, little endian
	 * 
	 * output each name of item, in the order of the walk above, separated by '\n'
	 * [compressed with the Deflate algorithm]
	 * 
	 * Chunk 3
	 * 4B length of chunk 3, in bytes, little endian
	 * 
	 * output IV+MAC of each file, in the order of the walk above (until EOF)
	 */
	private boolean save() {
		try {
			synchronized (MAGIC) {
				File f = new File(baseDir, TMP_FILE);
				FileOutputStream fos = new FileOutputStream(f, false);

				//write magic
				fos.write(MAGIC);

				//write the iv
				byte[] iv = new byte[16];
				PRNG.getInstance().getBytes(iv);
				fos.write(iv);

				//encrypted part
				PhelixOutputStream out = new PhelixOutputStream(fos, password,
						iv, false);

				//magic num again
				out.write(MAGIC);

				//rnd seed
				byte[] rndSeed = new byte[16];
				PRNG.getInstance().getBytes(rndSeed);
				out.write(rndSeed);

				out.write(getKey());

				//<write the filesystem contents>
				BitArrayOutputStream chunk1 = new BitArrayOutputStream();

				ByteArrayOutputStream chunk2Orig = new ByteArrayOutputStream();
				DeflaterOutputStream chunk2 = new DeflaterOutputStream(
						chunk2Orig);

				ByteArrayOutputStream chunk3 = new ByteArrayOutputStream();

				writeDir(root, chunk1, chunk2, chunk3);

				byte[] len = new byte[4];
				byte[] toWrite;

				toWrite = chunk1.getByteArray();
				Utils.encodeLittleEndian32(toWrite.length, len, 0);
				out.write(len);
				out.write(toWrite);

				chunk2.finish();
				chunk2.close();
				toWrite = chunk2Orig.toByteArray();
				Utils.encodeLittleEndian32(toWrite.length, len, 0);
				out.write(len);
				out.write(toWrite);

				toWrite = chunk3.toByteArray();
				Utils.encodeLittleEndian32(toWrite.length, len, 0);
				out.write(len);
				out.write(toWrite);

				//</write the filesystem contents>

				out.flush();
				out.close();

				fos.write(out.getMac());
				fos.close();

				File dest = new File(baseDir, TOC_FILE);
				File bak = new File(baseDir, BAK_FILE);
				bak.delete();
				dest.renameTo(bak);
				return f.renameTo(dest);
			}
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	private void writeDir(FSDirectory dir, BitArrayOutputStream chunk1,
			OutputStream chunk2, OutputStream chunk3) throws IOException {
		FSItem[] items = dir.getContents();

		for (FSItem item : items) {
			if (FSUtils.isDir(item)) {
				chunk1.putBit(0);
				chunk1.putBit(0);

				chunk2.write(item.getName().getBytes());
				chunk2.write('\n');

				writeDir((FSDirectory) item, chunk1, chunk2, chunk3);
			} else {
				chunk1.putBit(1);

				chunk2.write(item.getName().getBytes());
				chunk2.write('\n');

				chunk3.write(((CFSFile) item).getIv());
				chunk3.write(((CFSFile) item).getMac());
			}
		}

		chunk1.putBit(0);
		chunk1.putBit(1);
	}

	private void load() throws GenericIOException {
		InputStream fis = null;
		try {
			fis = new FileInputStream(new File(baseDir, TOC_FILE));

			if (fis.available() < 88)
				throw new GenericIOException(
						"Malformed file: truncation detected.");

			byte[] magic = new byte[4];
			fis.read(magic);
			if (!Arrays.equals(magic, MAGIC))
				throw new GenericIOException(
						"Malformed file: wrong magic number.");

			byte[] iv = new byte[16];
			fis.read(iv);

			PhelixInputStream in = new PhelixInputStream(fis, password, iv, fis
					.available() - 16 /* MAC */, false);

			in.read(magic);
			if (!Arrays.equals(magic, MAGIC))
				throw new WrongPassException();

			byte[] rndSeed = new byte[16];
			in.read(rndSeed);
			PRNG.getInstance().reSeed(rndSeed);

			key = new byte[32];
			in.read(key);

			byte[] bLen = new byte[4];
			in.read(bLen);
			int len = Utils.decodeLittleEndian32(bLen, 0);
			byte[] rawChunk1 = new byte[len];
			in.read(rawChunk1);

			in.read(bLen);
			len = Utils.decodeLittleEndian32(bLen, 0);
			byte[] rawChunk2 = new byte[len];
			in.read(rawChunk2);

			in.read(bLen);
			len = Utils.decodeLittleEndian32(bLen, 0);
			byte[] chunk3 = new byte[len];
			in.read(chunk3);

			in.close();

			byte[] mac = in.getMac();
			byte[] mac2 = new byte[mac.length];
			fis.read(mac2);
			fis.close();

			if (!Arrays.equals(mac, mac2))
				throw new GenericIOException(
						"Malformed file: corrupted filesystem.");

			BitArrayInputStream chunk1 = new BitArrayInputStream(rawChunk1);

			BufferedReader chunk2 = new BufferedReader(
					new InputStreamReader(new InflaterInputStream(
							new ByteArrayInputStream(rawChunk2))));

			int bit, idx = 0;
			byte[] fileIV, fileMac;
			String name;
			while (true) {
				bit = chunk1.getBit();

				if (bit < 0) //end
					break;
				else if (bit == 0) { //it's a directory boundary
					bit = chunk1.getBit();
					if (bit == 0) { //start of dir content, create and go down
						name = chunk2.readLine().trim();
						CFSDirectory dir = new CFSDirectory(pwd, name);
						pwd.add(dir);
						pwd = dir;
					} else { //end of dir content, go up
						pwd = pwd.getParent();
					}
				} else { //it's a file
					name = chunk2.readLine().trim();

					fileIV = new byte[5];
					System.arraycopy(chunk3, idx, fileIV, 0, 5);
					idx += 5;

					fileMac = new byte[Phelix.PHELIX_MAC_SIZE >> 3];
					System.arraycopy(chunk3, idx, fileMac, 0, fileMac.length);
					idx += fileMac.length;

					anchorFile(name, fileIV, fileMac);
				}
			}

			chunk2.close();
		} catch (IOException e) {
			throw new GenericIOException("In loading TOC file", e);
		} finally {
			if (fis != null)
				try {
					fis.close();
				} catch (IOException e) {
				}
		}

		pwd = root;
	}
}
